<script setup lang="ts">
import {ref, onMounted, reactive, onUnmounted} from 'vue'
import dIcon from '@/components/dIcon.vue'
import {
  CaptchaProps,
  CaptchaEmits,
  PI,
  sum,
  square,
  getRandomNumberByRange
} from './dCaptcha'
const props = defineProps(CaptchaProps)
const emit = defineEmits(CaptchaEmits)

interface State {
  sliderActive: boolean
  sliderSuccess: boolean
  sliderFail: boolean
  canvasCtx: CanvasRenderingContext2D | null
  blockCtx: CanvasRenderingContext2D | null
  blockX: number
  blockY: number
  L: number
  originX: number
  originY: number
  isMouseDown: boolean
  trail: number[]
  sliderLeft: number | string
  sliderMaskWidth: number | string
  success: boolean
  loadFlag: boolean
}

const state = reactive<State>({
  sliderActive: false,
  sliderSuccess: false,
  sliderFail: false,
  canvasCtx: null,
  blockCtx: null,
  blockX: 0,
  blockY: 0,
  L: props.l + props.r * 2 + 3,
  originX: 0,
  originY: 0,
  isMouseDown: false,
  trail: [],
  sliderLeft: '0px',
  sliderMaskWidth: 0,
  success: false,
  loadFlag: false
})

const block = ref()
const canvas = ref()

const imgRef = ref<HTMLImageElement>()
const imgError = ref(false)

const initDom = () => {
  state.canvasCtx = canvas.value.getContext('2d', {willReadFrequently: true})
  state.blockCtx = block.value.getContext('2d', {willReadFrequently: true})
}

const verify = () => {
  const arr = state.trail // 拖拽时y轴的轨迹坐标
  const average = arr.reduce(sum) / arr.length // 纵坐标的平均值
  const deviations = arr.map((y) => y - average) // 纵坐标与其平均值的插值数组
  const stddev = Math.sqrt(deviations.map(square).reduce(sum) / arr.length) // 标准差
  const left = parseInt(block.value.style.left)
  const accuracy =
    props.accuracy <= 1 ? 1 : props.accuracy > 10 ? 10 : props.accuracy

  return {
    isPass: Math.abs(left - state.blockX) <= accuracy,
    isRobot: average !== stddev
  }
}

const getRandomBg = (): string => {
  const len = props.bgList.length - 1
  return props.bgList[getRandomNumberByRange(0, len)] as string
}

const createImg = (onload) => {
  const img = new Image()
  img.crossOrigin = 'Anonymous'
  img.onload = onload
  img.onerror = (error) => {
    console.error('Background image failed to load', error)
    imgError.value = true
    // img.src = getRandomBg() + '?time=' + new Date().valueOf()
  }
  img.src = getRandomBg() + '?time=' + new Date().valueOf()
  return img
}

const draw = (ctx, x, y, type) => {
  const {l, r} = props
  ctx.beginPath()
  ctx.moveTo(x, y)
  ctx.arc(x + l / 2, y - r + 2, r, 0.72 * PI, 2.26 * PI)
  ctx.lineTo(x + l, y)
  ctx.arc(x + l + r - 2, y + l / 2, r, 1.21 * PI, 2.78 * PI)
  ctx.lineTo(x + l, y + l)
  ctx.lineTo(x, y + l)
  ctx.arc(x + r - 2, y + l / 2, r + 0.4, 2.76 * PI, 1.24 * PI, true)
  ctx.lineTo(x, y)
  ctx.lineWidth = 2
  ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'
  ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)'
  ctx.stroke()
  ctx[type]()
  ctx.globalCompositeOperation = 'destination-over'
}

const drawBlock = () => {
  state.blockX = getRandomNumberByRange(state.L + 10, props.w - (state.L + 10))
  state.blockY = getRandomNumberByRange(props.r + 10, props.h - (state.L + 10))
  draw(state.canvasCtx, state.blockX, state.blockY, 'fill')
  draw(state.blockCtx, state.blockX, state.blockY, 'clip')
}

const initImg = () => {
  const img: HTMLImageElement = createImg(() => {
    state.loadFlag = false
    imgError.value = false
    drawBlock()
    state.canvasCtx && state.canvasCtx.drawImage(img, 0, 0, props.w, props.h)
    state.blockCtx && state.blockCtx.drawImage(img, 0, 0, props.w, props.h)

    const _y = state.blockY - props.r * 2 - 1
    const imageDate = state.blockCtx?.getImageData(
      state.blockX,
      _y,
      state.L,
      state.L
    ) as ImageData
    block.value.width = state.L
    state.blockCtx?.putImageData(imageDate, 0, _y)
  })
  imgRef.value = img
}

const reset = () => {
  state.success = false
  state.sliderFail = false
  state.sliderSuccess = false
  state.sliderFail = false
  state.sliderLeft = 0
  block.value.style.left = 0
  state.sliderMaskWidth = 0
  // canvas
  const {w, h} = props
  state.canvasCtx?.clearRect(0, 0, w, h)
  state.blockCtx?.clearRect(0, 0, w, h)
  block.value.width = w

  // generate img
  imgRef.value &&
    (imgRef.value.src = getRandomBg() + '?time=' + new Date().valueOf())
  emit('onFulfilled')
}

const refresh = () => {
  reset()
  emit('onRefresh')
}

const moveEvt = (e) => {
  if (!state.isMouseDown) return
  const moveX = e.clientX - state.originX
  const moveY = e.clientY - state.originY
  if (moveX < 0 || moveX + 40 >= props.w) return
  state.sliderLeft = moveX + 'px'
  const blockLeft = ((props.w - 40 - 20) / (props.w - 40)) * moveX
  block.value.style.left = blockLeft + 'px'

  state.sliderActive = true // add active
  state.sliderMaskWidth = moveX + 5 + 'px'
  state.trail.push(moveY)
}
const upEvt = (e) => {
  if (!state.isMouseDown) return
  state.isMouseDown = false
  if (e.clientX === state.originX) return
  state.sliderActive = false // remove active

  const {isPass, isRobot} = verify()
  if (isPass) {
    if (props.accuracy === -1) {
      state.sliderSuccess = true
      state.success = true
      emit('onVerifySuccess')
      return
    }
    if (isRobot) {
      // succ
      state.sliderSuccess = true
      state.success = true
      emit('onVerifySuccess')
    } else {
      state.sliderFail = true
      emit('onVerifyAgain')
    }
  } else {
    state.sliderFail = true
    emit('onVerifyFail')
    setTimeout(() => {
      reset()
    }, 1000)
  }
}

const bindEvt = () => {
  document.addEventListener('mousemove', moveEvt)
  document.addEventListener('mouseup', upEvt)
}

const init = () => {
  initDom()
  initImg()
  bindEvt()
}

const sliderDown = (e) => {
  if (state.success) return
  state.originX = e.clientX
  state.originY = e.clientY
  state.isMouseDown = true
}

const touchStartEvt = (e) => {
  if (state.success) return
  state.originX = e.changedTouches[0].pageX
  state.originY = e.changedTouches[0].pageY
  state.isMouseDown = true
}
const touchMoveEvt = (e) => {
  if (!state.isMouseDown) return
  const moveX = e.changedTouches[0].pageX - state.originX
  const moveY = e.changedTouches[0].pageY - state.originY
  if (moveX < 0 || moveX + 38 >= props.w) return
  state.sliderLeft = moveX + 'px'
  const blockLeft = ((props.w - 40 - 20) / (props.w - 40)) * moveX
  block.value.style.left = blockLeft + 'px'

  state.sliderActive = true
  state.sliderMaskWidth = moveX + 'px'
  state.trail.push(moveY)
}
const touchEndEvt = (e) => {
  if (!state.isMouseDown) return
  state.isMouseDown = false
  if (e.changedTouches[0].pageX === state.originX) return
  state.sliderActive = false

  const {isPass, isRobot} = verify()
  if (isPass) {
    if (props.accuracy === -1) {
      state.sliderSuccess = true
      state.success = true
      emit('onVerifySuccess')
      return
    }
    if (isRobot) {
      // succ
      state.sliderSuccess = true
      state.success = true
      emit('onVerifySuccess')
    } else {
      state.sliderFail = true
      emit('onVerifyAgain')
    }
  } else {
    state.sliderFail = true
    emit('onVerifyFail')
    setTimeout(() => {
      reset()
    }, 1000)
  }
}

onMounted(() => {
  init()
})

onUnmounted(() => {
  document.removeEventListener('mousemove', moveEvt)
  document.removeEventListener('mouseup', upEvt)
})
</script>

<template>
  <div class="sv-box" :style="{width: w + 'px'}" onselectstart="return false">
    <!-- 加载背景图片遮罩层 -->
    <div :class="{'bg-mask': state.loadFlag}" />
    <d-icon
      :class="['sv-refresh', {'refresh-icon': imgError}]"
      :icon-config="{
        icon: 'captcha-refresh',
        type: 'custom',
        size: 25
      }"
      :hover="true"
      @click="refresh"
    />
    <!-- 画布 -->
    <div class="slider-canvas">
      <canvas ref="canvas" :width="w" :height="h" />
      <canvas ref="block" :width="w" :height="h" class="canvas-block" />
      <!-- 错误提示 -->
      <div v-if="imgError" class="canvas-img-error">
        <span>图片加载失败，请点击刷新按钮重试!</span>
      </div>
    </div>
    <!-- 滑动条容器 -->
    <div
      class="slider"
      :class="{
        active: state.sliderActive,
        success: state.sliderSuccess,
        fail: state.sliderFail
      }"
    >
      <div class="slideslider-mask" :style="{width: state.sliderMaskWidth}">
        <!-- 滑动块 -->
        <div
          class="slider-inner"
          :style="{left: state.sliderLeft}"
          @mousedown="sliderDown"
          @touchstart="touchStartEvt"
          @touchmove="touchMoveEvt"
          @touchend="touchEndEvt"
        >
          <d-icon
            class="slider-icon"
            :icon-config="{
              icon: state.sliderSuccess
                ? 'captcha-success'
                : state.sliderFail
                  ? 'captcha-fail'
                  : 'captcha-right',
              type: 'custom',
              color: '#ffffff'
            }"
            :size="16"
            :hover="true"
          />
        </div>
      </div>
      <span class="slider-text">{{ text }}</span>
    </div>
  </div>
</template>

<style lang="less" scoped>
.sv-box {
  position: relative;
  .bg-mask {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    background: rgba(255, 255, 255, 0.9);
    z-index: 999;
    animation: loading 1.5s infinite;
  }

  @keyframes loading {
    0% {
      opacity: 0.7;
    }
    100% {
      opacity: 1;
    }
  }

  .slider-canvas {
    position: relative;
    margin-bottom: 10px;
  }
  .canvas-block {
    position: absolute;
    left: 0;
    top: 0;
  }

  .slider-title {
    height: 40px;
    line-height: 40px;
    padding-left: 10px;
    background: rgba(255, 255, 255, 0.9);
    font-size: 18px;
  }
  .sv-refresh {
    position: absolute;
    right: 5px;
    top: 5px;
    z-index: 999;
    stroke: #ffffff;
    fill: #ffffff;
  }

  .canvas-img-error {
    display: flex;
    flex-direction: column;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    align-items: center;
    justify-content: center;
    span {
      white-space: nowrap;
    }
  }

  .refresh-icon {
    stroke: #707070;
    fill: #707070;
  }

  .slider {
    position: relative;
    text-align: center;
    width: 100%;
    height: 40px;
    line-height: 40px;
    color: #45494c;
    border: 1px solid #e4e7eb;
    border-radius: 4px;
    background: #eeeeee;
    .slideslider-mask {
      position: absolute;
      left: -1px;
      top: -1px;
      height: 40px;
      background: transparent;
      .slider-inner {
        position: absolute;
        top: 0px;
        left: 0px;
        width: 40px;
        height: 40px;
        background: #f7f9fa;
        border: 1px solid #dcdee2;
        border-radius: 4px;
        cursor: pointer;
        transition: background 0.2s linear;
        .slider-icon {
          z-index: 2;
          stroke: #707070;
        }
        &:hover {
          background: var(--theme-color);
          .slider-icon {
            stroke: #ffffff;
          }
        }
      }
    }
  }

  .active .slideslider-mask {
    border: 1px solid var(--theme-color);
  }

  .active .slider-inner {
    top: -1px !important;
    border: 1px solid var(--theme-color);
    background-color: var(--theme-color) !important;
  }

  .success .slideslider-mask {
    border: 1px solid #52ccba;
    background-color: #cef8f2;
  }

  .success .slider-inner {
    top: -1px !important;
    border: 1px solid #52ccba;
    background-color: #52ccba !important;
  }

  .success .slider-icon {
    background-position: 0 0 !important;
  }

  .fail .slideslider-mask {
    border: 1px solid #f57a7a;
    background-color: #fce1e1;
  }

  .fail .slider-inner {
    top: -1px !important;
    border: 1px solid #f57a7a;
    background-color: #f57a7a !important;
  }

  .active .slider-text,
  .success .slider-text,
  .fail .slider-text {
    display: none;
  }
}
</style>
